/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is ThumbsDown.
 *
 * The Initial Developer of the Original Code is Atte Kemppilä.
 * Portions created by the Initial Developer are Copyright (C) 2011
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


const EXPORTED_SYMBOLS = ["Thumbnail"];

const Ci = Components.interfaces;

const CLASS_HIGHLIGHT = "thumbsdown_selected";
const P_TIMER_ID = "_thumbsdown_timerId";
const ID_IMAGEMAP_CANVAS_ = "_thumbsdown_canvas_";
const STYLE_IMAGEMAP_CANVAS_FILL = "rgba(0, 0, 255, 0.4)";


const Thumbnail =
{
    getThumbnails: function(node)
    {
        if (node instanceof Ci.nsIDOMHTMLDocument)
            return this._getDocumentThumbnails(node);
        
        if (node instanceof Ci.nsIDOMRange)
            return this._getRangeThumbnails(node);
        
        if (node instanceof Ci.nsIDOMHTMLLIElement)
            return this._getListItemThumbnails(node);
        
        return this._getElementThumbnails(node);
    },
    
    _getDocumentThumbnails: function(doc)
    {
        let walker = doc.createTreeWalker(doc, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, null, true);
        return this._collectThumbnails(walker);
    },
    
    _getRangeThumbnails: function(range)
    {
        let commonAncestor = range.commonAncestorContainer;
        let doc = commonAncestor.ownerDocument;
        let filter = function(element)
        {
            let delta = range.comparePoint(element, 0);
            switch (delta)
            {
                case -1: return Ci.nsIDOMNodeFilter.FILTER_SKIP;
                case 0:  return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
                case 1:  return Ci.nsIDOMNodeFilter.FILTER_REJECT;
                default: throw new Error(delta);
            }
        };
        
        let walker = doc.createTreeWalker(commonAncestor, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, filter, true);
        return this._collectThumbnails(walker);
    },
    
    _getListItemThumbnails: function(li)
    {
        let thumbnails = [];
        let element = li.firstElementChild;
        while (element)
        {
            Array.prototype.push.apply(thumbnails, this._getElementThumbnails(element));
            element = element.nextElementSibling;
        }
        
        return thumbnails;
    },
    
    _collectThumbnails: function(walker)
    {
        let thumbnails = [];
        while (walker.nextNode())
            Array.prototype.push.apply(thumbnails, this._getElementThumbnails(walker.currentNode));
        
        return thumbnails;
    },
    
    _getElementThumbnails: function(element)
    {
        let thumbnails = [];
        
        if (element instanceof Ci.nsIDOMHTMLImageElement)
            thumbnails.push(new ImgThumbnail(element));
        
        if (element instanceof Ci.nsIDOMHTMLAnchorElement)
        {
            thumbnails.push(new CssBackgroundImageThumbnail(element, element));
            thumbnails.push(new CssBackgroundImageThumbnail(element, element.parentNode));
        }
        
        if (element instanceof Ci.nsIDOMHTMLAreaElement)
            thumbnails.push(new ImageMapThumbnail(element));
        
        thumbnails = [thumbnail for each (thumbnail in thumbnails) if (thumbnail.isThumbnail())];
        return thumbnails;
    },
}


function BaseThumbnail(element)
{
    this.element = element;
}

BaseThumbnail.prototype =
{
    get win() { return this.element.ownerDocument.defaultView; },
    get doc() { return this.element.ownerDocument; },
    
    equals: function(thumbnail)
    {
        return (
            thumbnail
            && (this.type == thumbnail.type)
            && (this.thumbnailUrl == thumbnail.thumbnailUrl)
            && (this.linkUrl == thumbnail.linkUrl));
    },
    
    isSame: function(thumbnail)
    {
        return (
            thumbnail
            && (this.type == thumbnail.type)
            && (this.element == thumbnail.element));
    },
}


function ImageMapThumbnail(area)
{
    BaseThumbnail.call(this, area);
    
    this.area = area;
}

ImageMapThumbnail.prototype =
{
    __proto__: BaseThumbnail.prototype,
    
    type: "image-map",
    
    get thumbnailUrl() { return this.img.src; },
    get linkUrl() { return this.area.href; },
    get thumbnailImage() { return this.img; },
    
    get map() { if (! ("_map" in this)) this._map = getAncestorNode(this.area, Ci.nsIDOMHTMLMapElement); return this._map; },
    get img() { if (! ("_img" in this)) this._img = this.doc.querySelector("img[usemap='#" + this.map.name + "']"); return this._img; },
    
    isThumbnail: function()
    {
        try
        {
            return (
                (this.area instanceof Ci.nsIDOMHTMLAreaElement)
                && this.img.src.length);
        }
        catch (e)
        {
            return false;
        }
    },
    
    highlight: function(enable)
    {
        toggleClass(this.area, CLASS_HIGHLIGHT, enable);
        
        if (this.map[P_TIMER_ID])
            return;
        
        let self = this;
        let update = function()
        {
            try { self._updateCanvas(); } catch (e) { /* ignore */ }
            self.map[P_TIMER_ID] = null;
        };
        this.map[P_TIMER_ID] = this.doc.defaultView.setTimeout(update, 0);
    },
    
    _updateCanvas: function()
    {
        // get highlight <canvas>...
        let canvasId = ID_IMAGEMAP_CANVAS_ + this.map.name;
        let canvas = this.doc.getElementById(canvasId);
        if (! canvas)
        {
            // ...or create one if one doesn't exist
            let img = this.img;
            canvas = this.doc.createElement("canvas");
            canvas.setAttribute("id", canvasId);
            canvas.setAttribute("width", img.width);
            canvas.setAttribute("height", img.height);
            canvas.setAttribute("style", "position: absolute; pointer-events: none;");
            img.parentNode.insertBefore(canvas, img);
        }
        
        // clear highlight <canvas>
        let ctx = canvas.getContext("2d");
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        // re-draw each highlighted <area> to <canvas>
        let areas = this.map.querySelectorAll("area." + CLASS_HIGHLIGHT);
        for (let i = 0; i < areas.length; i++)
        {
            let area = areas[i];
            let shape = area.shape.substr(0, 4).toLowerCase();
            let coords = [parseFloat(x) for each (x in area.coords.split(","))];
            
            ctx.beginPath();
            switch (shape)
            {
                case "rect":
                    ctx.rect(coords[0], coords[1], coords[2] - coords[0], coords[3] - coords[1]);
                    break;
                case "poly":
                    ctx.moveTo(coords[0], coords[1]);
                    for (let k = 2; k < coords.length; k += 2)
                        ctx.lineTo(coords[k], coords[k + 1]);
                    break;
                case "circ":
                    ctx.arc(coords[0], coords[1], coords[2], 0, Math.PI * 2, false);
                    break;
            }
            ctx.closePath();
            
            ctx.fillStyle = STYLE_IMAGEMAP_CANVAS_FILL;
            ctx.fill();
        }
    }
}


function ImgThumbnail(img)
{
    BaseThumbnail.call(this, img);
    
    this.img = img;
}

ImgThumbnail.prototype =
{
    __proto__: BaseThumbnail.prototype,
    
    type: "img",
    
    get thumbnailUrl() { return this.img.src; },
    get linkUrl() { let anchor = this.anchor; return (anchor ? anchor.href : "") },
    get thumbnailImage() { return this.img; },
    
    get anchor() { if (! ("_anchor" in this)) this._anchor = getAncestorNode(this.img, Ci.nsIDOMHTMLAnchorElement, 2); return this._anchor; },
    
    isThumbnail: function()
    {
        try
        {
            return (
                (this.img instanceof Ci.nsIDOMHTMLImageElement)
                && this.img.src.length);
        }
        catch (e)
        {
            return false;
        }
    },
    
    highlight: function(enable)
    {
        toggleClass(this.img, CLASS_HIGHLIGHT, enable);
    }
}


function CssBackgroundImageThumbnail(element, bgElement)
{
    BaseThumbnail.call(this, element);
    
    this.bgElement = bgElement;
}

CssBackgroundImageThumbnail.prototype =
{
    __proto__: BaseThumbnail.prototype,
    
    type: "css-background-image",
    
    get thumbnailUrl()
    {
        if (! ("_thumbnailUrl" in this))
        {
            this._thumbnailUrl = "";
            
            let cssValueList = this.win.getComputedStyle(this.bgElement).getPropertyCSSValue("background-image");
            if (cssValueList.length > 0)
            {
                let value = cssValueList[0];
                if (value.primitiveType == Ci.nsIDOMCSSPrimitiveValue.CSS_URI)
                    this._thumbnailUrl = value.getStringValue();
            }
        }
        
        return this._thumbnailUrl;
    },
    
    get thumbnailImage()
    {
        if (! this._image)
        {
            this._image = new this.win.Image();
            this._image.src = this.thumbnailUrl;
        }
        
        return this._image;
    },
    
    isThumbnail: function()
    {
        try
        {
            return (
                (this.bgElement instanceof Ci.nsIDOMHTMLElement)
                && this.thumbnailUrl.length);
        }
        catch (e)
        {
            return false;
        }
    },
    
    highlight: function(enable)
    {
        toggleClass(this.bgElement, CLASS_HIGHLIGHT, enable);
    }
}


function getAncestorNode(node, ancestorType, maxDepth)
{
    let depth = maxDepth || 1000;
    node = node.parentNode;
    
    while (node && (! (node instanceof ancestorType)) && (--depth > 0))
        node = node.parentNode;
    
    return ((depth > 0) ? node : null);
}

function toggleClass(node, className, enable)
{
    if (enable)
        node.classList.add(className);
    else
        node.classList.remove(className);
}
